/*
 *   Copyright (c) 2020-2021 Project CHIP Authors
 *   All rights reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

/**
 *    @file
 *      Implementation of JNI bridge for CHIP Device Controller for Android apps
 *
 */
#include <jni.h>
#include <lib/core/CHIPError.h>
#include <lib/support/CHIPJNIError.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/JniReferences.h>
#include <lib/support/JniTypeWrappers.h>
#include <lib/support/logging/CHIPLogging.h>
#include <platform/CHIPDeviceConfig.h>
#include <platform/ConfigurationManager.h>
#include <platform/ConnectivityManager.h>
#include <platform/KeyValueStoreManager.h>
#include <platform/internal/BLEManager.h>
#include <platform/internal/NFCCommissioningManager.h>

#include <android/log.h>

#include "AndroidChipPlatform-JNI.h"
#include "BLEManagerImpl.h"
#include "BleConnectCallback-JNI.h"
#include "CommissionableDataProviderImpl.h"
#include "DiagnosticDataProviderImpl.h"
#include "DnssdImpl.h"
#include "NFCCommissioningManagerImpl.h"
#include "tracing.h"

using namespace chip;

#define JNI_METHOD(RETURN, METHOD_NAME) extern "C" JNIEXPORT RETURN JNICALL Java_chip_platform_AndroidChipPlatform_##METHOD_NAME
#define JNI_LOGGING_METHOD(RETURN, METHOD_NAME)                                                                                    \
    extern "C" JNIEXPORT RETURN JNICALL Java_chip_platform_AndroidChipLogging_##METHOD_NAME
#define JNI_MDNSCALLBACK_METHOD(RETURN, METHOD_NAME)                                                                               \
    extern "C" JNIEXPORT RETURN JNICALL Java_chip_platform_ChipMdnsCallbackImpl_##METHOD_NAME

#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
static bool JavaBytesToUUID(JNIEnv * env, jbyteArray value, chip::Ble::ChipBleUUID & uuid);
#endif

namespace {
JavaVM * sJVM = nullptr;
JniGlobalReference sAndroidChipPlatformExceptionCls;
jmethodID sOnLogMessageMethod = nullptr;
JniGlobalReference sJavaLogCallbackObject;

// Java object containing the listener to notify when a service is resolved.
// It can be used by an Android application to be notified when the
//  Operational discovery is done.
jobject mJListenerObject          = nullptr;
jmethodID mServiceResolveListener = nullptr;

} // namespace

CHIP_ERROR AndroidChipPlatformJNI_OnLoad(JavaVM * jvm, void * reserved)
{
    CHIP_ERROR err = CHIP_NO_ERROR;
    JNIEnv * env;

    ChipLogProgress(DeviceLayer, "AndroidChipPlatform JNI_OnLoad() called");

    TEMPORARY_RETURN_IGNORED chip::Platform::MemoryInit();

    // Save a reference to the JVM.  Will need this to call back into Java.
    JniReferences::GetInstance().SetJavaVm(jvm, "chip/platform/AndroidChipPlatform");
    sJVM = jvm;

    // Get a JNI environment object.
    env = JniReferences::GetInstance().GetEnvForCurrentThread();
    VerifyOrExit(env != NULL, err = CHIP_JNI_ERROR_NO_ENV);

    ChipLogProgress(DeviceLayer, "Loading Java class references.");

    // Get various class references need by the API.

    jclass androidChipPlatformException;
    err = JniReferences::GetInstance().GetLocalClassRef(env, "chip/platform/AndroidChipPlatformException",
                                                        androidChipPlatformException);
    SuccessOrExit(err);
    err = sAndroidChipPlatformExceptionCls.Init(static_cast<jobject>(androidChipPlatformException));
    SuccessOrExit(err);

    ChipLogProgress(DeviceLayer, "Java class references loaded.");

    err = BleConnectCallbackJNI_OnLoad(jvm, reserved);
    SuccessOrExit(err);

    chip::Android::InitializeTracing();
exit:
    if (err != CHIP_NO_ERROR)
    {
        JniReferences::GetInstance().ThrowError(env, sAndroidChipPlatformExceptionCls, err);
        JNI_OnUnload(jvm, reserved);
    }

    return err;
}

void AndroidChipPlatformJNI_OnUnload(JavaVM * jvm, void * reserved)
{
    chip::Android::ShutdownTracing();

    ChipLogProgress(DeviceLayer, "AndroidChipPlatform JNI_OnUnload() called");
    BleConnectCallbackJNI_OnUnload(jvm, reserved);
    chip::Platform::MemoryShutdown();
}

JNI_METHOD(void, initChipStack)(JNIEnv * env, jobject self)
{
    chip::DeviceLayer::StackLock lock;
    CHIP_ERROR err = chip::DeviceLayer::PlatformMgr().InitChipStack();
    VerifyOrReturn(err == CHIP_NO_ERROR,
                   ChipLogError(DeviceLayer, "Error initializing CHIP stack: %" CHIP_ERROR_FORMAT, err.Format()));
}

// for NFCCommissioningManager
JNI_METHOD(void, nativeSetNFCCommissioningManager)(JNIEnv * env, jobject, jobject manager)
{
    ChipLogProgress(DeviceLayer, "(Android JNI) nativeSetNFCCommissioningManager()");

    VerifyOrReturn(env != nullptr, ChipLogError(DeviceLayer, "nativeSetNFCCommissioningManager(): Invalid env"));

#if CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING
    chip::DeviceLayer::StackLock lock;
    chip::DeviceLayer::Internal::NFCCommissioningMgrImpl().InitializeWithObject(manager);
#endif // CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING
}

JNI_METHOD(void, onNfcTagResponse)(JNIEnv * env, jobject self, jbyteArray jbArray)
{
    ChipLogProgress(Controller, "%p (Android JNI) onNfcTagResponse()", self);

    VerifyOrReturn(env != nullptr, ChipLogError(DeviceLayer, "onNfcTagResponse(): Invalid env"));

#if CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING
    chip::DeviceLayer::StackLock lock;

    jbyte * data = env->GetByteArrayElements(jbArray, nullptr);
    jsize length = env->GetArrayLength(jbArray);

    System::PacketBufferHandle buffer =
        System::PacketBufferHandle::NewWithData(reinterpret_cast<const uint8_t *>(data), static_cast<size_t>(length));
    VerifyOrReturn(!buffer.IsNull(), ChipLogError(DeviceLayer, "Failed to allocate packet buffer"));

    TEMPORARY_RETURN_IGNORED chip::DeviceLayer::Internal::NFCCommissioningMgrImpl().OnNfcTagResponse(Transport::PeerAddress::NFC(),
                                                                                                     std::move(buffer));
#endif // CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING
}

JNI_METHOD(void, onNfcTagError)(JNIEnv * env, jobject self)
{
    ChipLogProgress(DeviceLayer, "(Android JNI) onNfcTagError()");

    VerifyOrReturn(env != nullptr, ChipLogError(DeviceLayer, "onNfcTagError(): Invalid env"));

#if CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING
    chip::DeviceLayer::StackLock lock;
    TEMPORARY_RETURN_IGNORED chip::DeviceLayer::Internal::NFCCommissioningMgrImpl().OnNfcTagError(Transport::PeerAddress::NFC());
#endif // CHIP_DEVICE_CONFIG_ENABLE_NFC_BASED_COMMISSIONING
}

// for BLEManager
JNI_METHOD(void, nativeSetBLEManager)(JNIEnv *, jobject, jobject manager)
{
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
    chip::DeviceLayer::StackLock lock;
    chip::DeviceLayer::Internal::BLEMgrImpl().InitializeWithObject(manager);
#endif // CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
}

JNI_METHOD(void, handleWriteConfirmation)
(JNIEnv * env, jobject self, jint conn, jbyteArray svcId, jbyteArray charId)
{
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
    chip::DeviceLayer::StackLock lock;
    BLE_CONNECTION_OBJECT const connObj = reinterpret_cast<BLE_CONNECTION_OBJECT>(conn);

    chip::Ble::ChipBleUUID svcUUID;
    chip::Ble::ChipBleUUID charUUID;
    VerifyOrReturn(JavaBytesToUUID(env, svcId, svcUUID),
                   ChipLogError(DeviceLayer, "handleWriteConfirmation() called with invalid service ID"));
    VerifyOrReturn(JavaBytesToUUID(env, charId, charUUID),
                   ChipLogError(DeviceLayer, "handleWriteConfirmation() called with invalid characteristic ID"));

    chip::DeviceLayer::Internal::BLEMgrImpl().HandleWriteConfirmation(connObj, &svcUUID, &charUUID);
#endif
}

JNI_METHOD(void, handleIndicationReceived)
(JNIEnv * env, jobject self, jint conn, jbyteArray svcId, jbyteArray charId, jbyteArray value)
{
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
    chip::DeviceLayer::StackLock lock;
    BLE_CONNECTION_OBJECT const connObj = reinterpret_cast<BLE_CONNECTION_OBJECT>(conn);
    const auto valueBegin               = env->GetByteArrayElements(value, nullptr);
    const auto valueLength              = env->GetArrayLength(value);

    chip::Ble::ChipBleUUID svcUUID;
    chip::Ble::ChipBleUUID charUUID;
    chip::System::PacketBufferHandle buffer;

    VerifyOrExit(JavaBytesToUUID(env, svcId, svcUUID),
                 ChipLogError(DeviceLayer, "handleIndicationReceived() called with invalid service ID"));
    VerifyOrExit(JavaBytesToUUID(env, charId, charUUID),
                 ChipLogError(DeviceLayer, "handleIndicationReceived() called with invalid characteristic ID"));

    buffer = System::PacketBufferHandle::NewWithData(valueBegin, valueLength);
    VerifyOrExit(!buffer.IsNull(), ChipLogError(DeviceLayer, "Failed to allocate packet buffer"));

    chip::DeviceLayer::Internal::BLEMgrImpl().HandleIndicationReceived(connObj, &svcUUID, &charUUID, std::move(buffer));
exit:
    env->ReleaseByteArrayElements(value, valueBegin, 0);
#endif
}

JNI_METHOD(void, handleSubscribeComplete)
(JNIEnv * env, jobject self, jint conn, jbyteArray svcId, jbyteArray charId)
{
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
    chip::DeviceLayer::StackLock lock;
    BLE_CONNECTION_OBJECT const connObj = reinterpret_cast<BLE_CONNECTION_OBJECT>(conn);

    chip::Ble::ChipBleUUID svcUUID;
    chip::Ble::ChipBleUUID charUUID;
    VerifyOrReturn(JavaBytesToUUID(env, svcId, svcUUID),
                   ChipLogError(DeviceLayer, "handleSubscribeComplete() called with invalid service ID"));
    VerifyOrReturn(JavaBytesToUUID(env, charId, charUUID),
                   ChipLogError(DeviceLayer, "handleSubscribeComplete() called with invalid characteristic ID"));

    chip::DeviceLayer::Internal::BLEMgrImpl().HandleSubscribeComplete(connObj, &svcUUID, &charUUID);
#endif
}

JNI_METHOD(void, handleUnsubscribeComplete)
(JNIEnv * env, jobject self, jint conn, jbyteArray svcId, jbyteArray charId)
{
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
    chip::DeviceLayer::StackLock lock;
    BLE_CONNECTION_OBJECT const connObj = reinterpret_cast<BLE_CONNECTION_OBJECT>(conn);

    chip::Ble::ChipBleUUID svcUUID;
    chip::Ble::ChipBleUUID charUUID;
    VerifyOrReturn(JavaBytesToUUID(env, svcId, svcUUID),
                   ChipLogError(DeviceLayer, "handleUnsubscribeComplete() called with invalid service ID"));
    VerifyOrReturn(JavaBytesToUUID(env, charId, charUUID),
                   ChipLogError(DeviceLayer, "handleUnsubscribeComplete() called with invalid characteristic ID"));

    chip::DeviceLayer::Internal::BLEMgrImpl().HandleUnsubscribeComplete(connObj, &svcUUID, &charUUID);
#endif
}

JNI_METHOD(void, handleConnectionError)(JNIEnv * env, jobject self, jint conn)
{
#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
    chip::DeviceLayer::StackLock lock;
    BLE_CONNECTION_OBJECT const connObj = reinterpret_cast<BLE_CONNECTION_OBJECT>(conn);

    chip::DeviceLayer::Internal::BLEMgrImpl().HandleConnectionError(connObj, BLE_ERROR_APP_CLOSED_CONNECTION);
#endif
}

// for KeyValueStoreManager
JNI_METHOD(void, setKeyValueStoreManager)(JNIEnv * env, jclass self, jobject manager)
{
    chip::DeviceLayer::StackLock lock;
    chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl().InitializeWithObject(manager);
}

// for ConfigurationManager
JNI_METHOD(void, setConfigurationManager)(JNIEnv * env, jclass self, jobject manager)
{
    chip::DeviceLayer::StackLock lock;
    chip::DeviceLayer::ConfigurationManagerImpl::GetDefaultInstance().InitializeWithObject(manager);
}

// for DiagnosticDataProviderManager
JNI_METHOD(void, setDiagnosticDataProviderManager)(JNIEnv * env, jclass self, jobject manager)
{
    chip::DeviceLayer::StackLock lock;
    chip::DeviceLayer::DiagnosticDataProviderImpl::GetDefaultInstance().InitializeWithObject(manager);
}

// for ServiceResolver and  ServiceBrowser
JNI_METHOD(void, nativeSetDnssdDelegates)(JNIEnv * env, jclass self, jobject resolver, jobject browser, jobject chipMdnsCallback)
{
    chip::DeviceLayer::StackLock lock;
    chip::Dnssd::InitializeWithObjects(resolver, browser, chipMdnsCallback);
}

JNI_LOGGING_METHOD(void, setLogFilter)(JNIEnv * env, jclass clazz, jint level)
{
    using namespace chip::Logging;

    uint8_t category = kLogCategory_Detail;
    switch (level)
    {
    case ANDROID_LOG_VERBOSE:
    case ANDROID_LOG_DEBUG:
        category = kLogCategory_Detail;
        break;
    case ANDROID_LOG_INFO:
        category = kLogCategory_Progress;
        break;
    case ANDROID_LOG_WARN:
    case ANDROID_LOG_ERROR:
        category = kLogCategory_Error;
        break;
    default:
        break;
    }
    SetLogFilter(category);
}

static void ENFORCE_FORMAT(3, 0) logRedirectCallback(const char * module, uint8_t category, const char * msg, va_list args)
{
    using namespace chip::Logging;

    JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
    VerifyOrReturn(env != nullptr);
    VerifyOrReturn(sJavaLogCallbackObject.HasValidObjectRef());
    VerifyOrReturn(sOnLogMessageMethod != nullptr);

    JniLocalReferenceScope scope(env);
    int priority = ANDROID_LOG_DEBUG;
    switch (category)
    {
    case kLogCategory_Error:
        priority = ANDROID_LOG_ERROR;
        break;
    case kLogCategory_Progress:
        priority = ANDROID_LOG_INFO;
        break;
    case kLogCategory_Detail:
        priority = ANDROID_LOG_DEBUG;
        break;
    default:
        break;
    }

    jint jPriority = static_cast<jint>(priority);
    jobject jModule;
    VerifyOrReturn(JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString(module), jModule) == CHIP_NO_ERROR);
    VerifyOrReturn(jModule != nullptr);

    char buffer[CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE];
    vsnprintf(buffer, sizeof(buffer), msg, args);
    jobject jMsg;
    VerifyOrReturn(JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString(buffer), jMsg) == CHIP_NO_ERROR);
    VerifyOrReturn(jMsg != nullptr);

    env->CallVoidMethod(sJavaLogCallbackObject.ObjectRef(), sOnLogMessageMethod, static_cast<jstring>(jModule), jPriority,
                        static_cast<jstring>(jMsg));
}

JNI_LOGGING_METHOD(void, setLogCallback)(JNIEnv * env, jclass clazz, jobject callback)
{
    using namespace chip::Logging;

    if (sOnLogMessageMethod == nullptr)
    {
        jclass callbackClass = env->GetObjectClass(callback);
        sOnLogMessageMethod  = env->GetMethodID(callbackClass, "onLogMessage", "(Ljava/lang/String;ILjava/lang/String;)V");
    }
    VerifyOrReturn(sOnLogMessageMethod != nullptr,
                   ChipLogError(DeviceLayer, "Failed to access AndroidChipLogging.LogCallback 'onLogMessage' method"));

    if (sJavaLogCallbackObject.HasValidObjectRef())
    {
        sJavaLogCallbackObject.Reset();
    }

    if (env->IsSameObject(callback, NULL))
    {
        SetLogRedirectCallback(nullptr);
    }
    else
    {
        VerifyOrReturn(sJavaLogCallbackObject.Init(callback) == CHIP_NO_ERROR,
                       ChipLogError(DeviceLayer, "Failed to init sJavaLogCallbackObject"));
        SetLogRedirectCallback(logRedirectCallback);
    }
}

JNI_MDNSCALLBACK_METHOD(void, handleServiceResolve)
(JNIEnv * env, jclass self, jstring instanceName, jstring serviceType, jstring hostName, jstring address, jint port,
 jobject attributes, jlong callbackHandle, jlong contextHandle)
{
    using ::chip::Dnssd::HandleResolve;
    HandleResolve(instanceName, serviceType, hostName, address, port, attributes, callbackHandle, contextHandle);

    if (mServiceResolveListener != nullptr)
    {
        env->CallVoidMethod(mJListenerObject, mServiceResolveListener, instanceName, serviceType);
    }
}

JNI_MDNSCALLBACK_METHOD(void, handleServiceBrowse)
(JNIEnv * env, jclass self, jobjectArray instanceName, jstring serviceType, jlong callbackHandle, jlong contextHandle)
{
    using ::chip::Dnssd::HandleBrowse;
    HandleBrowse(instanceName, serviceType, callbackHandle, contextHandle);
}

#if CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
static bool JavaBytesToUUID(JNIEnv * env, jbyteArray value, chip::Ble::ChipBleUUID & uuid)
{
    const auto valueBegin  = env->GetByteArrayElements(value, nullptr);
    const auto valueLength = env->GetArrayLength(value);
    bool result            = true;

    VerifyOrExit(valueBegin && valueLength == sizeof(uuid.bytes), result = false);
    memcpy(uuid.bytes, valueBegin, valueLength);

exit:
    env->ReleaseByteArrayElements(value, valueBegin, 0);
    return result;
}
#endif

// for CommissionableDataProvider
JNI_METHOD(jboolean, updateCommissionableDataProviderData)
(JNIEnv * env, jclass self, jstring spake2pVerifierBase64, jstring Spake2pSaltBase64, jint spake2pIterationCount,
 jlong setupPasscode, jint discriminator)
{
    chip::DeviceLayer::StackLock lock;
    CHIP_ERROR err = CommissionableDataProviderMgrImpl().Update(env, spake2pVerifierBase64, Spake2pSaltBase64,
                                                                spake2pIterationCount, setupPasscode, discriminator);
    if (err != CHIP_NO_ERROR)
    {
        ChipLogError(DeviceLayer, "Failed to update commissionable data provider data: %" CHIP_ERROR_FORMAT, err.Format());
        return false;
    }

    return true;
}

JNI_METHOD(void, setServiceResolveListener)
(JNIEnv * env, jclass self, jobject jListenerObject)
{
    VerifyOrReturn(env != nullptr, ChipLogError(DeviceLayer, "setServiceResolveListener(): Invalid env"));

    // Clean up old global ref if it exists
    if (mJListenerObject != nullptr)
    {
        env->DeleteGlobalRef(mJListenerObject);
        mJListenerObject = nullptr;
    }
    mServiceResolveListener = nullptr;

    if (jListenerObject == nullptr)
    {
        // Listener disabled
        return;
    }

    mJListenerObject = env->NewGlobalRef(jListenerObject);
    if (mJListenerObject == nullptr)
    {
        ChipLogError(Controller, "Failed to create weak global ref for listener");
        JniReferences::GetInstance().ThrowError(env, sAndroidChipPlatformExceptionCls, CHIP_ERROR_INTERNAL);
        return;
    }

    jclass listenerClass = env->GetObjectClass(jListenerObject);
    if (listenerClass == nullptr)
    {
        ChipLogError(Controller, "Failed to get listener class");

        // If an exception is already pending, do not throw another one
        if (env->ExceptionCheck())
        {
            // An exception occured
            return;
        }

        JniReferences::GetInstance().ThrowError(env, sAndroidChipPlatformExceptionCls, CHIP_ERROR_INTERNAL);
        return;
    }

    mServiceResolveListener = env->GetMethodID(listenerClass, "onServiceResolve", "(Ljava/lang/String;Ljava/lang/String;)V");

    // Clean up local reference to listenerClass
    env->DeleteLocalRef(listenerClass);

    if (mServiceResolveListener == nullptr)
    {
        ChipLogError(Controller, "Failed to access listener 'onServiceResolve' method");

        // If an exception is already pending, do not throw another one
        if (env->ExceptionCheck())
        {
            // An exception occured
            return;
        }

        JniReferences::GetInstance().ThrowError(env, sAndroidChipPlatformExceptionCls, CHIP_ERROR_INTERNAL);
        return;
    }
}
